개요 자바 메모리 구조 코드→메모리 가비지 컬렉션 자바스크립트 더 공부할 곳 📝 문제풀이
◀ CH 16 📋 목차
🧠 부록 · 심화

메모리, 코드가 실제로 사는 곳

변수 하나, 객체 하나가 어디에 올라가고, 언제 지워지는지를 알면 성능·버그·GC가 한눈에 이해돼요. 자바의 스택·힙·메서드 영역부터 자바스크립트 메모리까지, 코드와 함께 차근차근 봅니다.

🎯 이 장을 끝내면
📖
개요
왜 메모리를 알아야 할까?
프로그램이 돈다는 건, 결국 "메모리 위에서" 도는 거예요.

프로그램이 실행되면, 필요한 데이터(변수·객체)명령(코드)이 전부 메모리(RAM)에 올라가고, CPU가 그걸 읽으며 일해요. 그래서 "변수를 만든다", "객체를 생성한다"는 말은 사실 메모리 어딘가에 자리를 잡는다는 뜻이에요.

자바와 자바스크립트는 둘 다 메모리를 자동으로 관리해줘요(가비지 컬렉션). 그래서 C처럼 직접 반납하지 않아도 되지만, 어디에 뭐가 올라가고 언제 지워지는지를 알면 — 성능 문제, 메모리 누수, "왜 값이 안 바뀌지?" 같은 버그가 훨씬 잘 보여요.

🧭
큰 그림 먼저. 메모리는 용도에 따라 몇 개의 구역으로 나뉘어요. 자바는 크게 스택(임시 작업대) · 힙(객체 창고) · 메서드 영역(설계도·공용품 보관소)으로 나뉜다고 기억하면 나머지가 쉽게 붙어요.
🏛️
자바 메모리 구조
JVM 런타임 데이터 영역
JVM이 프로그램을 돌릴 때 쓰는 메모리는 5개 영역으로 나뉘어요.

자바 프로그램은 JVM(자바 가상 머신) 위에서 돌아요. JVM은 실행에 필요한 메모리를 아래처럼 런타임 데이터 영역(Runtime Data Area)으로 나눠 씁니다.

📚 메서드 영역 (Method Area)
클래스의 설계도가 올라가는 곳. 클래스 정보(필드·메서드 정의), static 변수, 상수(리터럴), 메서드 바이트코드를 보관해요.
모든 스레드 공유 프로그램 내내 유지
📦 힙 (Heap)
new로 만든 모든 객체·배열이 사는 창고. 크고, GC가 청소하는 대상이에요. 어느 스레드에서든 접근 가능해요.
모든 스레드 공유 GC 대상
🧮 스택 (Stack)
메서드 호출마다 쌓이는 임시 작업대. 지역변수·매개변수·연산 중간값이 여기 담겨요. 메서드가 끝나면 통째로 사라져요.
스레드마다 1개 자동 정리
🖼️ 그림으로 보기 — JVM 메모리 구조 한눈에
JVM 런타임 데이터 영역 (Runtime Data Area) 🔗 모든 스레드가 공유 📚 메서드 영역 (Method Area) · 클래스 정보(필드·메서드 정의) · static 변수 · 상수(리터럴) · 메서드 바이트코드 프로그램 내내 유지 📦 힙 (Heap) · new 로 만든 모든 객체 · 배열 · GC(가비지 컬렉션)가 청소하는 영역 Young (신규) Old (오래 생존) 🧵 스레드마다 하나씩 🧮 스택 (Stack) 메서드 호출마다 프레임이 쌓임 greet() 프레임 · 지역변수 main() 프레임 · 지역변수 메서드 끝나면 프레임 pop (자동 정리) 🧭 PC 레지스터 · 현재 명령 주소 🔧 네이티브 메서드 스택 · C 등
왼쪽 메서드 영역·힙모든 스레드가 함께 쓰는 공용 공간이고, 오른쪽 스택·PC 레지스터·네이티브 스택스레드마다 따로 가져요. 그래서 여러 스레드가 같은 객체(힙)를 공유하다 충돌이 생길 수 있는 거예요(→ CS기초 '동시성' 참고).

나머지 둘은 보조 역할이라 이름만 알아두면 돼요. PC 레지스터(현재 실행 중인 명령의 주소를 기억, 스레드마다 하나)와 네이티브 메서드 스택(자바가 아닌 C 등으로 짜인 코드를 부를 때 쓰는 스택)이에요.

🍳 비유 — 주방으로 보면 메서드 영역 = 요리책·공용 양념장 📚(모두가 같이 보는 설계도·상수), 힙 = 냉장고·창고 📦(재료·완성품 = 객체를 넣어두는 큰 저장 공간), 스택 = 지금 쓰는 도마 🧮(요리(메서드) 하나 할 때만 잠깐 재료를 올려놓는 작업대). 요리(메서드)가 끝나면 도마(스택 프레임)는 싹 치우지만, 냉장고(힙)에 넣어둔 건 남아 있어요.
🔀
핵심 구분
기본형은 스택에, 객체는 힙에

이게 자바 메모리에서 가장 중요한 구분이에요. 기본형(primitive) 변수는 값 자체를 스택에 담고, 참조형(객체) 변수는 스택에 주소(참조)만 담고 실제 객체는 에 둬요.

int age = 20;                 // 기본형: 값 20이 스택에 그대로
int[] nums = new int[]{1, 2}; // 참조형: 배열 객체는 힙에, nums에는 그 '주소'만
구분변수에 담기는 것실제 데이터 위치
기본형int, long, double, char, boolean …값 그 자체스택
참조형객체, 배열, String …힙에 있는 객체의 주소
💡
"참조를 복사한다"는 말의 뜻. Person a = new Person(); Person b = a; 하면, 객체가 복사되는 게 아니라 주소만 복사돼요. a와 b가 같은 힙 객체를 가리키니, b로 값을 바꾸면 a로 봐도 바뀌어 있어요.
🎬
코드 → 메모리
한 줄씩, 어디에 올라가고 언제 지워지나
실제 코드로 스택·힙이 어떻게 채워지고 비워지는지 따라가 봐요.
class Person {
    String name;
    int age;
    Person(String name, int age) { this.name = name; this.age = age; }
}

public class Main {
    static int count = 0;              // static 변수

    public static void main(String[] args) {
        int a = 10;                    // (1)
        Person p = new Person("Lee", 20); // (2)
        greet(p);                      // (3)
    }                                  // (5) main 끝

    static void greet(Person person) { // (3)
        String msg = "Hi " + person.name; // (4)
        System.out.println(msg);
    }                                  // (4-끝) greet 끝
}
0프로그램 시작
Main 클래스가 로딩되면서 클래스 정보와 static 변수 count메서드 영역에 자리 잡아요. count는 프로그램이 끝날 때까지 여기 살아 있어요.
1int a = 10;
main() 스택 프레임이 스택에 생기고, 그 안에 지역변수 a = 10(값 자체)이 담겨요.
2Person p = new Person("Lee", 20);
new가 실행되면 Person 객체가 힙에 생성돼요(name·age를 가진 실제 데이터). 지역변수 p는 스택에 있고, 그 객체의 주소만 담아요. 생성자 실행 동안엔 생성자용 프레임이 잠깐 스택에 올랐다가 끝나면 사라져요. (문자열 "Lee"는 힙의 문자열 상수 풀에 보관돼요.)
3greet(p); 호출
greet() 프레임이 스택 위에 새로 쌓여요. 매개변수 person에는 p가 가진 주소가 복사돼요. 그래서 person과 p는 같은 힙 객체를 가리켜요(객체가 복사된 게 아님).
4String msg = ...; 그리고 greet 끝
msg는 greet 프레임의 지역변수예요. println이 끝나고 greet가 return되면 — greet 프레임이 통째로 스택에서 사라져요(pop). msg와 person도 함께 없어져요. 하지만 힙의 Person 객체는 아직 p가 가리키고 있어 남아 있어요.
5main 끝
main 프레임이 pop 되면서 a·p가 사라져요. 이제 힙의 Person 객체를 가리키는 참조가 하나도 없어요GC가 회수 대상으로 표시하고, 나중에 청소해요.
🖼️ 그림으로 보기 — greet(p) 실행 중인 순간
📚 메서드 영역 · Method Area static count = 0 · class Main · class Person (프로그램 내내 유지) 🧮 스택 (Stack) — 호출마다 위로 쌓임 greet() 프레임 ▲ 최근 호출 person msg = "Hi Lee" main() 프레임 a = 10 (값 자체) p 메서드가 끝나면 그 프레임은 통째로 사라져요 (pop). 📦 힙 (Heap) Person 객체 (new) name age = 20 (값 자체) "Lee" String (문자열 상수 풀)
personp같은 Person 객체를 가리켜요(주소만 복사). Person의 name 은 힙의 문자열 "Lee"를 가리키고요. age·a=10 같은 기본형은 값이 그대로 들어 있어요. 화살표(→)는 전부 "주소로 가리킴(참조)"을 뜻해요.
📌
한 문장 요약. 지역변수는 스택에서 메서드가 끝나면 자동으로 사라지고, new로 만든 객체는 힙에 남아 있다가 아무도 안 가리키게 되면 GC가 지워요. static은 메서드 영역에서 프로그램 내내 유지돼요.
🧹
가비지 컬렉션
누가 쓰레기고, 어떻게 치우나
자바가 힙을 대신 청소해주는 원리예요.

가비지 컬렉션(GC)은 힙에서 더 이상 쓰이지 않는 객체를 찾아 자동으로 메모리를 회수하는 기능이에요. 기준은 딱 하나 — "지금 도달할 수 있는가(reachable)"예요.

🔗 도달 가능성(reachability) GC Root(스택의 지역변수, static 변수 등 "확실히 살아 있는 시작점")에서 참조를 타고 타고 닿을 수 있는 객체는 살아 있는 것으로 봐요. 어떤 참조로도 닿을 수 없으면 쓰레기(garbage)로 판단해 회수해요. 그래서 p = null; 처럼 참조를 끊으면, 그 객체는 도달 불가가 되어 GC 대상이 돼요.

대표 알고리즘은 마크 앤 스윕(Mark and Sweep)이에요. ① Root에서 닿는 객체를 전부 표시(mark)하고, ② 표시 안 된 것들을 쓸어(sweep) 회수해요. 이후 흩어진 공간을 모으는 압축(compact)을 하기도 해요.

👶
세대별 GC
"대부분의 객체는 금방 죽는다"

경험적으로 새로 만든 객체 대부분은 아주 빨리 안 쓰이게 돼요(약한 세대 가설). 그래서 힙을 나이대로 나눠 관리해요.

영역담기는 것청소
Young (Eden + Survivor)갓 태어난 객체Minor GC — 자주, 빠르게
Old (Tenured)Young에서 오래 살아남은 객체Major(Full) GC — 드물게, 무겁게
🖼️ 그림으로 보기 — 도달 가능성(reachability)
GC Root 스택 변수 · static 객체 A ✓ 살아있음 객체 B ✓ 살아있음 객체 C 🗑 회수 대상 객체 D 🗑 회수 대상 Root에서 C·D로는 못 닿음
GC Root에서 화살표를 타고 닿을 수 있는 A·B는 살아있어요. C·D는 서로 참조(C→D)해도 Root에서 못 닿으니 회수 대상이에요. 그래서 "순환 참조"만으론 안 지워지는 게 아니라, 도달 가능성이 기준인 거예요.
💡
개발자는 직접 메모리를 반납하지 않아요(C의 free 같은 게 없음). System.gc()로 "요청"은 할 수 있지만 강제는 아니에요 — 실제 시점은 JVM이 정해요. 우리가 할 일은 안 쓰는 참조를 오래 붙들지 않는 것뿐이에요.
🟨
자바스크립트
JS의 메모리 — 자바와 닮은 듯 다른
JS 엔진(V8 등)도 스택과 힙을 쓰고, GC로 자동 정리해요.

자바스크립트도 콜 스택(Call Stack)힙(Heap)을 써요. 자바와 마찬가지로 원시값은 스택에 값으로, 객체는 힙에 두고 변수엔 참조를 담아요.

let a = 10;            // 원시값(number): 값 자체가 스택에
let obj = { x: 1 };    // 객체: 힙에 저장, obj엔 '참조'만
let obj2 = obj;        // 참조 복사 → 같은 객체를 가리킴
obj2.x = 99;
console.log(obj.x);    // 99  (같은 객체라 함께 바뀜)

let b = a;             // 원시값은 값 복사
b = 50;
console.log(a);        // 10  (원본은 그대로)

원시값(number, string, boolean, null, undefined, symbol, bigint)은 복사하면 값이 통째로 복사돼서 서로 독립이에요. 객체·배열·함수는 참조가 복사돼서 같은 대상을 공유해요 — 자바의 기본형 vs 참조형과 똑같은 원리죠.

🧹 JS의 가비지 컬렉션 JS도 도달 가능성(reachability) 기반의 마크 앤 스윕으로 청소해요. Root(전역 객체, 현재 실행 중인 함수의 변수 등)에서 닿지 못하는 객체를 회수해요. 옛날 방식인 참조 카운팅은 서로를 참조하는 순환 참조를 못 지우는 문제가 있었어요. 그래서 현대 엔진은 도달 가능성 방식을 써요.
💧
주의
JS에서 메모리 누수가 나는 흔한 패턴

GC가 있어도 "참조가 남아 있으면" 못 지워요. 그래서 안 쓰는데도 계속 참조가 붙어 있는 상황이 곧 누수예요.

// 1) 실수로 만든 전역 변수 — 프로그램 내내 안 지워짐
function f() { leak = new Array(1000000); } // var/let 빠뜨림 → 전역

// 2) 안 지운 타이머 — 콜백이 계속 살아 있음
const id = setInterval(doWork, 1000);
// 다 쓰면 clearInterval(id) 로 꼭 해제

// 3) 클로저가 큰 데이터를 계속 붙들고 있는 경우
function make() {
  const big = new Array(1000000);
  return () => big[0]; // big이 계속 참조됨 → GC 못 함
}
🧯
누수를 줄이는 습관. 안 쓰는 타이머·이벤트 리스너는 해제(clearInterval, removeEventListener), 필요 없어진 큰 객체는 참조를 끊기(x = null), 전역 변수 남발 금지. 자바에서도 똑같이 — 안 쓰는 컬렉션에 객체를 계속 넣어두면 GC가 손대지 못해요.
자바자바스크립트
실행 환경JVMJS 엔진(V8 등)
임시 작업 공간스택(스레드마다)콜 스택
객체 저장
클래스·static·상수메서드 영역(별도 영역 개념 없음, 엔진이 관리)
메모리 정리GC (세대별·마크앤스윕)GC (마크앤스윕)
수동 해제없음없음
🧭
더 공부할 곳
메모리를 더 깊이 알고 싶다면
언어 메모리 말고도, 메모리를 이해하면 좋은 영역들이에요.

지금까지는 언어(자바·JS) 관점이었어요. 메모리는 더 아래(운영체제·하드웨어)와 옆(다른 언어)에서도 계속 등장해요. 방향만 잡아둘게요.

영역핵심 개념왜 알면 좋나
운영체제(OS) 메모리가상 메모리, 페이징, 스와핑, 단편화, MMU"힙·스택"이 실제 물리 메모리에 어떻게 매핑되는지의 바탕. (CS기초 · 메모리관리 챕터)
C / C++ 수동 메모리malloc/free, new/delete, 포인터, 참조GC가 대신 해주던 일을 직접 해보면 힙·스택이 확실해져요. 실수하면 누수·이중 해제·댕글링 포인터.
스택 오버플로우재귀가 너무 깊으면 스택 프레임이 한계 초과재귀·함수 호출이 스택을 쓴다는 걸 체감. (자바 StackOverflowError)
메모리 부족(힙)객체를 계속 쌓으면 힙 고갈대용량 처리·누수의 결과. (자바 OutOfMemoryError)
값 전달 vs 참조 전달pass by value / reference"함수에 넘긴 값이 왜 안(또는 왜) 바뀌지?"의 답. 스택·힙 이해가 핵심.
메모리 계층레지스터 > 캐시 > RAM > 디스크"왜 캐시가 중요한가", 성능의 바탕. (CS기초 · 컴퓨터 구조 챕터)
🔗
이 스터디 안에서도 이어집니다 — CS기초 트레이닝"메모리 관리"(OS 관점)"CS와 컴퓨터 구조"(캐시·계층) 챕터가 이 페이지의 아래층 이야기예요. 언어 관점(여기) + OS 관점(CS기초)을 같이 보면 그림이 완성돼요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
네 가지 유형(객관식 · O/X · 빈칸 · 코드결과)을 섞어 두었어요. 편하게 눌러보세요.